<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>16.4.1-解析标签节点</title>
</head>
<body>
    <div>代码示例作为参考，此页面代码不能执行</div>
</body>
    <script>
        /**
         * 统一使用parseTag 函数处理，并通过该函数的第二个参数来指定具体的处理类型
         * - 第二个参数值为字符串 'end' 时，意味着解析的是结束标签
         * - 无论处理的是开始标签还是结束标签，parseTag 函数都会消费对应的内容
         * 
         * parseTag 函数的实现
         * - 函数既用于解析开始标签，又用于解析结束标签，因此需要用一个参数来标识当前处理的标签类型，即 type
         * - 对于开始标签和结束标签，用于匹配它们的正则表达式只有一点不同：结束标签是以字符串 </ 开头的
         * 
         * parseTag 函数的另外几个关键点
         * - 在完成正则匹配后，需要调用 advanceBy 函数消费由正则匹配的全部内容
         * - 标签中可能存在无用的空白字符，例如 <div----|>，因此我们需要调用 advanceSpaces 函数消费空白字符
         * - 检查剩余模板内容是否以字符串 /> 开头。
         *    如果是，则说明当前解析的是一个自闭合标签，这时需要将标签节点的 isSelfClosing 属性设置为 true
         * - 最后，判断标签是否自闭合。如果是，则调用 advnaceBy 函数消费内容 />，否则只需要消费内容 > 即可
         * 
         * parseElement 函数在得到由 parseTag 函数产生的标签节点后，需要根据节点的类型完成文本模式的切换
        */


        // 定义文本模式，作为一个状态表
        const TextModes = {
            DATA: 'DATA', // 普通文本模式
            RCDATA: 'RCDATA', // 受限的 CDATA 模式
            RAWTEXT: 'RAWTEXT', // 原始文本模式
            CDATA: 'CDATA' // CDATA 模式
        }

        // 解析器函数，接收模板作为参数
        function parse(str) {
            // 定义上下文对象
            const context = {
                // source 是模板内容，用于在解析过程中进行消费
                source: str,
                // 解析器当前处于文本模式，初始模式为 DATA
                mode: TextModes.DATA,
                // advanceBy 函数用来消费指定数量的字符，它接收一个数字作为参数
                advanceBy(num) { // 新增 advanceBy 函数
                    // 根据给定字符数 num，截取位置 num 后的模板内容，并替换当前模板内容
                    context.source = context.source.slice(num)
                },
                // 无论是开始标签还是结束标签，都可能存在无用的空白字符，例如 <div    >
                advanceSpaces(){ // 新增 advanceSpaces 函数
                    // 匹配空白字符
                    const match = /^[\t\r\n\f ]+/.exec(context.source)
                    if (match) {
                        // 调用 advanceBy 函数消费空白字符
                        context.advanceBy(match[0].length)
                    }
                
                }
            }
            // 调用 parseChildren 函数开始进行解析，它返回解析后得到的子节点
            // parseChildren 函数接收两个参数：
            // 第一个参数是上下文对象 context
            // 第二个参数是由父代节点构成的节点栈，初始时栈为空
            const nodes = parseChildren(context, [])

            // 解析器返回 Root 根节点
            return {
                type: 'Root',
                // 使用 nodes 作为根节点的 children
                children: nodes
            }
        }

        function parseChildren(context, ancestors){
            // 定义 nodes 数组存储子节点，它将作为最终的返回值
            let nodes = []
            // 从上下文对象中取得当前状态，包括模式 mode 和模板内容 source
            const { mode, source } = context

            // 开启 while 循环，只要满足条件就会一直对字符串进行解析
            // 关于 isEnd() 后文会详细讲解
            while(!isEnd(context, ancestors)) {
                let node
                // 只有 DATA 模式和 RCDATA 模式才支持插值节点的解析
                if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
                    // 只有 DATA 模式才支持标签节点的解析
                    if (mode === TextModes.DATA && source[0] === '<') {
                        if (source[1] === '!') {
                            if (source.startsWith('<!--')) {
                                // 注释
                                node = parseComment(context)
                            } else if (source.startsWith('<![CDATA[')) {
                                // CDATA
                                node = parseCDATA(context, ancestors)
                            }
                        } else if (source[1] === '/') {
                            // 状态机遭遇了闭合标签，此时应该抛出错误，因为它缺少与之对应的开始标签
                            console.error('无效的结束标签')
                            continue
                        } else if (/[a-z]/i.test(source[1])) {
                            // 标签
                            node = parseElement(context, ancestors)
                        }
                    } else if (source.startsWith('{{')) {
                        // 解析插值
                        node = parseInterpolation(context)
                    }
                }

                // node 不存在，说明处于其他模式，即非 DATA 模式且非 RCDATA 模式
                // 这时一切内容都作为文本处理
                if (!node) {
                    // 解析文本节点
                    node = parseText(context)
                }

                // 将节点添加到 nodes 数组中
                nodes.push(node)
            }

            // 当 while 循环停止后，说明子节点解析完毕，返回子节点
            return nodes
        }

        function parseElement(context, ancestors){
            // 解析开始标签
            const element = parseTag(context)
            if (element.isSelfClosing) return element

            // 切换到正确的文本模式 新增 文本模式的切换判断
            if (element.tag === 'textarea' || element.tag === 'title') {
                // 如果由 parseTag 解析得到的标签是 <textarea> 或 <title>，则切换到 RCDATA 模式
                context.mode = TextModes.RCDATA
            } else if (/style|xmp|iframe|noembed|noframes|noscript/.test(element.tag)) {
                // 如果由 parseTag 解析得到的标签是：
                // <style>、<xmp>、<iframe>、<noembed>、<noframes>、<noscript>
                // 则切换到 RAWTEXT 模式
                context.mode = TextModes.RAWTEXT
            } else {
                // 否则切换到 DATA 模式
                context.mode = TextModes.DATA
            }

            ancestors.push(element)
            // 这里递归地调用 parseChildren 函数进行 <div> 标签子节点的解析
            element.children = parseChildren(context, ancestors)
            ancestors.pop()

            if (context.source.startsWith(`</${element.tag}`)) {
                // 解析结束标签
                // 再次调用 parseTag 函数解析结束标签，传递了第二个参数：'end'
                parseTag(context, 'end')
            } else {
                // 缺少闭合标签
                console.error(`${element.tag} 标签缺少闭合标签`)
            }
            
            return element
        }

        function isEnd(context, ancestors) {
            // 当模板内容解析完毕后，停止
            if (!context.source) return true
            // // 获取父级标签节点
            /**
             * <div><span></div></span>
             * -> 状态机 3”遭遇了不符合预期的状态，
             *    因为结束标签 </div>缺少与之对应的开始标签，
             *    所以这时“状态机 3”会抛出错误：“无效的结束标签”。
             * */ 
            // const parent = ancestors[ancestors.length - 1]
            // // 如果遇到结束标签，并且该标签与父级标签节点同名，则停止
            // if (parent && context.source.startsWith(`</${parent.tag}`)) {
            //     return true
            // }

            // 与父级节点栈内所有节点做比较
            for (let i = ancestors.length - 1; i >= 0; --i) {
                // 只要栈中存在与当前结束标签同名的节点，就停止状态机
                if (context.source.startsWith(`</${ancestors[i].tag}`)) {
                    return true
                }
            }
        }

        // 由于 parseTag 既用来处理开始标签，也用来处理结束标签，因此我们设计第二个参数 type，
        // 用来代表当前处理的是开始标签还是结束标签，type 的默认值为 'start'，即默认作为开始标签处理
        function parseTag(context, type = 'start') { // 新增 parseTag 函数
            // 从上下文对象中拿到 advanceBy 函数
            const { advanceBy, advanceSpaces } = context

            // 处理开始标签和结束标签的正则表达式不同
            const match = type === 'start'
                // 匹配开始标签
                ? /^<([a-z][^\t\r\n\f />]*)/i.exec(context.source)
                // 匹配结束标签
                : /^<\/([a-z][^\t\r\n\f />]*)/i.exec(context.source)
            // 匹配成功后，正则表达式的第一个捕获组的值就是标签名称
            const tag = match[1]
            // 消费正则表达式匹配的全部内容，例如 '<div' 这段内容
            advanceBy(match[0].length)
            // 消费标签中无用的空白字符
            advanceSpaces()

            // 在消费匹配的内容后，如果字符串以 '/>' 开头，则说明这是一个自闭合标签
            const isSelfClosing = context.source.startsWith('/>')
            // 如果是自闭合标签，则消费 '/>'， 否则消费 '>'
            advanceBy(isSelfClosing ? 2 : 1)

            // 返回标签节点
            return {
                type: 'Element',
                // 标签名称
                tag,
                // 标签的属性暂时留空
                props: [],
                // 子节点留空
                children: [],
                // 是否自闭合
                isSelfClosing
            }
        }


    </script>
    

</html>